------------------Ankh-----------------
A 4am crack                  2017-08-19
---------------------------------------

Name: Ankh
Genre: adventure
Year: 1983
Credits: David von Brink
Publisher: Datamost
Platform: Apple ][+ or later
Media: single-sided 5.25-inch floppy
OS: custom
Similar cracks:
  #1357 Mr. Robot and His Robot Factory

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  no errors, but copy displays an
  initial (graphical) screen then hangs
  with the disk motor on

Locksmith Fast Disk Backup
  ditto

EDD 4 bit copy (no sync, no count)
  ditto

Copy ][+ nibble editor
  nothing suspicious

Disk Fixer
  bootloader is custom (haha it sets
    the reset vector to $C65C then
    jumps to it to read the rest of
    track 0, that's hilarious)
  no disk catalog on any track
  no DOS
  everything is custom

Why didn't any of my copies work?
  I'm guessing there's a runtime
  protection check somewhere, and it's
  looping forever to find its initial
  condition (whatever that is).

Next steps:

  1. Find the check
  2. Disable the check
  3. Declare victory (*)

(*) go to the gym

                   ~

               Chapter 1
          How Do I Read Thee?
         Let Me Count The Ways


One thing that all protection checks
have in common is they need to turn on
the drive motor by accessing a specific
address in the $C0xx range. For slot 6,
it's $C0E9, but to allow disks to boot
from any slot, developers usually use
code like this:

  LDX <slot number x 16>
  LDA $C089,X

There's nothing that says where the
slot number has to be, although the
disk controller ROM routine uses zero
page $2B and lots of disks just reuse
that. There's also nothing that says
you have to use the X-register as the
index, or that you must use the
accumulator as the load register, or
even that you need to use an "LDA"
instuction. ("STA" works just as well.
I've even seen "CMP"!)

Also, since developers don't actually
want people finding their protection-
related code, they may try to encrypt
it or obfuscate it to prevent people
from finding it. But eventually, the
code must exist and the code must run,
and it must run on my machine, and I
have the final say on what my machine
does or does not do.

But sometimes you get lucky.

Turning to my trusty Disk Fixer sector
editor, I search the non-working copy
for "BD 89 C0", which is the opcode
sequence for "LDA $C089,X". And I find
it right away on track 0.

                 --v--

T00,S0F
----------- DISASSEMBLY MODE ----------
0000:A2 07          LDX   #$07
0002:A0 00          LDY   #$00
0004:A9 00          LDA   #$00

; check if slot peripheral space is
; writeable, by writing a #$00 to the
; first byte of each slot and checking
; if it worked
0006:8D 00 C7       STA   $C700
0009:AD 00 C7       LDA   $C700
000C:D0 06          BNE   $0014
000E:88             DEY
000F:D0 F3          BNE   $0004

; if that worked, jump to The Badlands
; to wipe main memory and reboot
0011:4C D5 B6       JMP   $B6D5

; check all 7 slots
0014:CE 08 B6       DEC   $B608
0017:CE 0B B6       DEC   $B60B
001A:CA             DEX
001B:D0 E5          BNE   $0002

(Fun fact: this check fails in some
modern emulators, because they emulate
non-existent cards by filling the slot
ROM with #$00 bytes which triggers a
false positive here.)

(Fun fact #2: I don't know what this
check is for. One theory was that it
was protection against hardware NMI
cards like the Wildcard which allowed
users to unconditionally break into the
monitor and save the contents of memory
despite the program's best efforts to
block it. But a friend tested this code
on a real Apple II with a Wildcard Plus
card installed, and it did not reboot.
So either that card specifically works
around this check, or the theory was
incorrect. Mysteries abound, even 30+
years later!)


                   ~

               Chapter 2
        How Do I (M)align Thee?
         Let Me Count The Ways


Continuing from $B61D...

                 --v--

001D:A2 FF          LDX   #$FF
001F:86 04          STX   $04
0021:E8             INX
0022:86 01          STX   $01
0024:86 03          STX   $03

; munge reset vector
0026:8E F2 03       STX   $03F2

; The self-modifying code in the slot
; checking loop tells me that this
; sector is loaded at $B600. Looking at
; it in a sector editor, it is pretty
; clear that there's an RWTS parameter
; table starting at $B6C0. Which means
; we're setting up a read (command $01)
; of track $01, sector $00.
0029:A2 01          LDX   #$01
002B:8E C4 B6       STX   $B6C4
002E:A9 01          LDA   #$01
0030:8D CC B6       STA   $B6CC

; Call the RWTS (not shown) to read
; T01,S00.
0033:20 B9 B6       JSR   $B6B9

; increment the track
0036:EE C4 B6       INC   $B6C4

; new RWTS command = 0 (seek), not read
0039:A9 00          LDA   #$00
003B:8D CC B6       STA   $B6CC

; seek to track 2
003E:20 B9 B6       JSR   $B6B9

; wait
0041:A9 44          LDA   #$44
0043:20 A8 FC       JSR   $FCA8

; turn on slot 6 drive motor manually
0046:AE F8 05       LDX   $05F8
0049:9D 89 C0       STA   $C089,X

; A is 0 coming out of the WAIT routine
; at $FCA8
004C:85 00          STA   $00

; increment Death Counter
004E:E6 00          INC   $00
0050:F0 33          BEQ   $0085

; find the next address prologue
0052:BD 8C C0       LDA   $C08C,X
0055:10 FB          BPL   $0052
0057:C9 D5          CMP   #$D5

; loop back if we didn't find a $D5
; nibble (increments Death Counter, so
; kind of important that we find one
; sooner rather than later)
0059:D0 F3          BNE   $004E
005B:BD 8C C0       LDA   $C08C,X
005E:10 FB          BPL   $005B
0060:C9 AA          CMP   #$AA
0062:D0 F3          BNE   $0057
0064:A0 02          LDY   #$02
0066:BD 8C C0       LDA   $C08C,X
0069:10 FB          BPL   $0066
006B:C9 96          CMP   #$96
006D:D0 E8          BNE   $0057

; get the physical sector number (loop
; burns through and discards the first
; two 4-and-4-encoded values in the
; address field, which are the disk
; volume number and the track number,
; then exits after parsing the sector
; number into zp$02)
006F:BD 8C C0       LDA   $C08C,X
0072:10 FB          BPL   $006F
0074:2A             ROL
0075:85 02          STA   $02
0077:BD 8C C0       LDA   $C08C,X
007A:10 FB          BPL   $0077
007C:25 02          AND   $02
007E:88             DEY
007F:10 EE          BPL   $006F

; is it sector 7?
0081:C9 07          CMP   #$07

; yes -> branch forward
0083:F0 04          BEQ   $0089

; no -> set a flag
0085:A2 FF          LDX   #$FF
0087:86 01          STX   $01

; Execution continues here regardless
; of which sector we found. Keep exact
; track of how long to took to find the
; next address prologue. Maximum number
; of nibbles ends up in zp$03; minimum
; number in zp$04.
0089:A5 00          LDA   $00
008B:C5 03          CMP   $03
008D:90 02          BCC   $0091
008F:85 03          STA   $03
0091:C5 04          CMP   $04
0093:B0 02          BCS   $0097
0095:85 04          STA   $04

; Loop back and do this all again for
; tracks $02-$10.
0097:AD C4 B6       LDA   $B6C4
009A:C9 11          CMP   #$11
009C:D0 90          BNE   $002E

; Calculate the difference between the
; minimum and maximum number of nibbles
; before each track's next address
; prologue.
009E:A5 03          LDA   $03
00A0:38             SEC
00A1:E5 04          SBC   $04
00A3:C9 20          CMP   #$20

; If the difference is less than #$20,
; branch forward.
00A5:90 04          BCC   $00AB

; Otherwise, set a flag.
00A7:A2 FF          LDX   #$FF
00A9:86 01          STX   $01

; Execution continues here regardless
; of the nibble count difference.
; Turn off the slot 6 drive motor.
00AB:AE F8 05       LDX   $05F8
00AE:9D 88 C0       STA   $C088,X

; Check that flag.
00B1:A5 01          LDA   $01

; If non-zero, some error occurred.
; Either we found the wrong sector on
; one of the tracks, or the difference
; in nibbles-before-address-prologue
; between tracks was too large.
00B3:F0 03          BEQ   $00B8

; Either way, wipe memory and reboot.
00B5:4C D5 B6       JMP   $B6D5

; Otherwise exit gracefully.
00B8:60             RTS

                   ~

               Chapter 3
         How Do I Patch Thee?
        Let Me Count The Bytes


Whew, that's quite a strict protection!
It require that tracks $01-$10 be
physically aligned, relative to each
other, so that "blindly" seeking to the
next track always finds sector $07. But
that's not all! It literally counts the
number of nibbles it takes to find the
start of sector $07 (after a "blind"
seek onto each track) and demands that
it not vary too much between tracks --
no more than $20 nibbles difference
between the minimum and maximum across
15 tracks!

This would be very difficult to
duplicate with a bit copier, even with
the "synchronize tracks" option. In
fact, I suspect this protection scheme
was specifically designed to test the
limits of that option. As there is no
sync sensor on standard Apple II floppy
drives, the "synchronize tracks" option
had to rely on seeking back to track 0,
finding a known pattern (like the start
of T00,S00), then forward to the next
track to write. This got less and less
precise as the track number increased,
because it spent more time seeking the
drive head between tracks, with only
approximate control over how far the
disk would spin before arriving at the
destination track.

The entire design of DOS (and ProDOS,
Pascal, and everything) compensates for
the fact that you never quite know
where you're going to end up on a track
when you get there. This copy protection
weaponizes that imprecision.

Anyway, it's a one-byte patch to change
the "JMP $B6D5" (which reboots) to "BIT
$B6D5" (which does nothing). After all
that work, it'll just fall through to
the success path after failing and
start the game anyway.

T00,S0F,$B5: 4C -> 2C

Oh, and one more byte so it doesn't
reboot if it finds a #$00 in the first
byte of a slot ROM. I would welcome any
feedback on what that was supposed to
protect against.

T00,S0F,$11: 4C -> 2C

Quod erat liberandum.

                   ~

               Epilogue


Some miscellaneous notes that don't
really fit anywhere else:

- You can't simply put an "RTS" at the
  beginning of this protection check,
  because the bootloader intentionally
  lies about the current track after
  the protection check returns. The
  disk's custom RWTS doesn't check if
  it's on the right track, so if the
  protection check never runs, it loads
  the rest of the game code from the
  wrong track and crashes.

- There is a second protection check on
  track $0C, sector $04 which is never
  loaded. As far as we can tell (and
  qkumba graciously volunteered to play
  test the ENTIRE GAME to verify this),
  the game is loaded into memory by the
  time you see the main title screen,
  and it never reads or writes from
  disk after that.

- The only widely circulated version of
  this game was an unfinished beta that
  said "THIS IS A PRELIMINARY VERSION"
  during the title sequence. All the
  hints and maps and documentation that
  were distributed by pirates back in
  the day were from this stolen beta.
  There were no known cracks of the
  retail version until a few years ago.
  This is the retail version.

---------------------------------------
A 4am crack                    No. 1367
------------------EOF------------------
